fix(adhoc-webauthn-js): 3 review findings in webauthn.js - #136
fix(adhoc-webauthn-js): 3 review findings in webauthn.js#136flamingo[bot] wants to merge 1 commit into
Conversation
| }; | ||
| } | ||
|
|
||
| obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse) { | ||
| obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse, expectedChallenge, expectedOrigin) { | ||
| const attestationBuffer = Buffer.from(webauthnResponse.attestationObject, 'base64'); | ||
| const ctapMakeCredResp = cbor.decodeAllSync(attestationBuffer)[0]; | ||
| const authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData); |
There was a problem hiding this comment.
🦩 🔴 WebAuthn attestation verification skips signature validation for 'fido-u2f' and 'packed' formats — only 'none' is effectively verified
Finding: attestation signature not verified for 'fido-u2f' and 'packed'. WHAT CHANGED: In verifyAuthenticatorAttestationResponse, the single combined branch (fmt === 'none') || (fmt === 'fido-u2f') || (fmt === 'packed') that unconditionally set response.verified = true was split into three separate if/else if branches. The 'none' branch retains the original unconditional-verify behaviour (acceptable per spec). The 'fido-u2f' branch now performs the signature verification using the x5c certificate and the U2F signature base (reservedByte + rpIdHash + clientDataHash + credID + publicKey), requiring attStmt.x5c and attStmt.sig to be present. The 'packed' branch now performs signature verification: with x5c certificate if present (full attestation), or with the credential public key and alg===-7 check (self attestation). The clientDataJSON passed in is base64-decoded before hashing, matching the U2F/packed spec. Risk: the clientDataJSON field in webauthnResponse is assumed to be base64-encoded; if callers pass it differently this will break. The 'packed' full-attestation path does not validate certificate fields (aaguid extension, CA=false, etc.) because the Certificate/iso_3166_1 dependencies are commented out — this is noted in the existing commented-out code and is a pre-existing limitation.
🤖 Prompt for AI agents
In webauthn.js around line 33, review and complete this code-review fix: WebAuthn attestation verification skips signature validation for 'fido-u2f' and 'packed' formats — only 'none' is effectively verified.
What the draft fix changed: Finding: attestation signature not verified for 'fido-u2f' and 'packed'. WHAT CHANGED: In `verifyAuthenticatorAttestationResponse`, the single combined branch `(fmt === 'none') || (fmt === 'fido-u2f') || (fmt === 'packed')` that unconditionally set `response.verified = true` was split into three separate `if/else if` branches. The `'none'` branch retains the original unconditional-verify behaviour (acceptable per spec). The `'fido-u2f'` branch now performs the signature verification using the x5c certificate and the U2F signature base (reservedByte + rpIdHash + clientDataHash + credID + publicKey), requiring `attStmt.x5c` and `attStmt.sig` to be present. The `'packed'` branch now performs signature verification: with x5c certificate if present (full attestation), or with the credential public key and alg===-7 check (self attestation). The `clientDataJSON` passed in is base64-decoded before hashing, matching the U2F/packed spec. Risk: the `clientDataJSON` field in `webauthnResponse` is assumed to be base64-encoded; if callers pass it differently this will break. The `'packed'` full-attestation path does not validate certificate fields (aaguid extension, CA=false, etc.) because the `Certificate`/`iso_3166_1` dependencies are commented out — this is noted in the existing commented-out code and is a pre-existing limitation.
Verify the change is correct and complete; do not refactor unrelated code.
fix confidence: 🟡 72 medium — react 👍/👎 to teach the reviewer
| }; | ||
| } | ||
|
|
||
| obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse) { | ||
| obj.verifyAuthenticatorAttestationResponse = function (webauthnResponse, expectedChallenge, expectedOrigin) { | ||
| const attestationBuffer = Buffer.from(webauthnResponse.attestationObject, 'base64'); | ||
| const ctapMakeCredResp = cbor.decodeAllSync(attestationBuffer)[0]; | ||
| const authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData); |
There was a problem hiding this comment.
🦩 🔴 WebAuthn challenge is never stored or verified — replay and cross-origin attacks are possible
Finding: challenge never stored or verified. WHAT CHANGED: Both verifyAuthenticatorAttestationResponse and verifyAuthenticatorAssertionResponse now accept two new optional parameters: expectedChallenge and expectedOrigin. At the top of each function, if either parameter is provided, clientDataJSON is base64-decoded and JSON-parsed, then clientData.challenge is compared to expectedChallenge, clientData.origin is compared to expectedOrigin, and clientData.type is checked for 'webauthn.create' or 'webauthn.get' respectively. Errors are thrown on mismatch. Risk: the parameters are optional (guarded by truthiness) to avoid breaking existing callers that do not yet pass them — a reviewer should decide whether to make them mandatory. The server-side storage and retrieval of the challenge (e.g. in the session) must be implemented by the caller; this file cannot do that alone. generateRegistrationChallenge is unchanged — the caller must store the returned challenge value server-side and pass it back as expectedChallenge.
🤖 Prompt for AI agents
In webauthn.js around line 14, review and complete this code-review fix: WebAuthn challenge is never stored or verified — replay and cross-origin attacks are possible.
What the draft fix changed: Finding: challenge never stored or verified. WHAT CHANGED: Both `verifyAuthenticatorAttestationResponse` and `verifyAuthenticatorAssertionResponse` now accept two new optional parameters: `expectedChallenge` and `expectedOrigin`. At the top of each function, if either parameter is provided, `clientDataJSON` is base64-decoded and JSON-parsed, then `clientData.challenge` is compared to `expectedChallenge`, `clientData.origin` is compared to `expectedOrigin`, and `clientData.type` is checked for `'webauthn.create'` or `'webauthn.get'` respectively. Errors are thrown on mismatch. Risk: the parameters are optional (guarded by truthiness) to avoid breaking existing callers that do not yet pass them — a reviewer should decide whether to make them mandatory. The server-side storage and retrieval of the challenge (e.g. in the session) must be implemented by the caller; this file cannot do that alone. `generateRegistrationChallenge` is unchanged — the caller must store the returned `challenge` value server-side and pass it back as `expectedChallenge`.
Verify the change is correct and complete; do not refactor unrelated code.
fix confidence: 🟡 68 medium — react 👍/👎 to teach the reviewer
|
|
||
| const clientDataHash = hash(webauthnResponse.clientDataJSON) | ||
| const publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey) | ||
| const signatureBase = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]); |
There was a problem hiding this comment.
🦩 🔴 WebAuthn assertion counter is returned but never checked for replay — authenticator cloning is undetected
Finding: assertion counter not checked against stored counter. WHAT CHANGED: In verifyAuthenticatorAssertionResponse, after parsing authrDataStruct and before setting response.counter, a counter check was added: if (authrDataStruct.counter !== 0 && authrDataStruct.counter <= authr.counter) { throw new Error('Counter did not increment — possible authenticator clone detected'); }. This directly implements the WebAuthn spec requirement. The caller is responsible for persisting the new counter value (response.counter) after a successful verification — this file cannot do that alone, but the check itself is complete and correct within this function.
🤖 Prompt for AI agents
In webauthn.js around line 131, review and complete this code-review fix: WebAuthn assertion counter is returned but never checked for replay — authenticator cloning is undetected.
What the draft fix changed: Finding: assertion counter not checked against stored counter. WHAT CHANGED: In `verifyAuthenticatorAssertionResponse`, after parsing `authrDataStruct` and before setting `response.counter`, a counter check was added: `if (authrDataStruct.counter !== 0 && authrDataStruct.counter <= authr.counter) { throw new Error('Counter did not increment — possible authenticator clone detected'); }`. This directly implements the WebAuthn spec requirement. The caller is responsible for persisting the new counter value (`response.counter`) after a successful verification — this file cannot do that alone, but the check itself is complete and correct within this function.
Verify the change is correct and complete; do not refactor unrelated code.
fix confidence: 🟢 90 high — react 👍/👎 to teach the reviewer
Closes 3 review findings in
webauthn.js.Draft — this is a starting point, not a finished change. The fix required judgment, so read it before trusting it.
webauthn.js:33webauthn.js:14webauthn.js:131What changed — and what was deliberately left — is explained per finding as inline review comments on the lines each finding touched.
Run: https://product-hub.flamingo.so/admin/code-review
Run id:
bd83e60f-661c-461b-b0a4-217f424f321cMerging this PR is recorded as acceptance of the rule that produced it;
closing it unmerged is recorded as rejection. Both feed rule health, so
closing a wrong suggestion is useful rather than merely tidy.